<!doctype html>
<html lang="zh">
<head>
  <meta charset="utf-8" />
  <title>SRS WebRTC 推流（浏览器摄像头）</title>
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <style>
    :root { font-family: system-ui, -apple-system, Segoe UI, Roboto, 'Helvetica Neue', Arial; }
    body { margin: 0; background:#0b1020; color:#e8ecf1; }
    .wrap { max-width: 980px; margin: 24px auto; padding: 0 16px; }
    h1 { font-size: 22px; margin: 0 0 12px; }
    .card { background:#121933; border:1px solid #1f2a4a; border-radius:16px; padding:16px; box-shadow: 0 6px 24px rgba(0,0,0,.25); }
    .grid { display:grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap:16px; }
    .row { display:flex; gap:12px; flex-wrap:wrap; align-items:center; }
    label { font-size: 12px; opacity:.85; display:block; margin-bottom:6px; }
    input, select { width:100%; padding:10px 12px; border-radius:12px; border:1px solid #2a355c; background:#0f1530; color:#e8ecf1; }
    input::placeholder{ color:#8aa0c7; }
    button { padding:10px 14px; border:0; border-radius:12px; background:#3b82f6; color:white; font-weight:600; cursor:pointer; }
    button.secondary { background:#334155; }
    button.danger { background:#ef4444; }
    button:disabled { opacity:.5; cursor:not-allowed; }
    .video-wrap { position:relative; border-radius:16px; overflow:hidden; background:#090f24; border:1px solid #1c2544; }
    video { width:100%; display:block; background:#000; }
    .badge { font-size:12px; opacity:.9; padding:4px 8px; border:1px solid #2a355c; border-radius:999px; }
    .log { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; background:#0a0f22; border:1px dashed #203056; border-radius:12px; padding:12px; height:180px; overflow:auto; }
    .hint { color:#92a7d0; font-size:13px; }
    .mt { margin-top:16px; }
  </style>
</head>
<body>
<div class="wrap">
  <h1>SRS WebRTC 推流（浏览器摄像头）</h1>

  <div class="card">
    <div class="grid">
      <div>
        <label>Server（主机或IP）</label>
        <input id="host" placeholder="例如: 192.168.1.10 或 srs.example.com" />
      </div>
      <div>
        <label>API 端口（SRS HTTP-API，默认 1985）</label>
        <input id="apiPort" type="number" value="1985" />
      </div>
      <div>
        <label>App</label>
        <input id="app" value="live" />
      </div>
      <div>
        <label>Stream（推流名）</label>
        <input id="stream" value="camera" />
      </div>
      <div>
        <label>分辨率</label>
        <select id="resolution">
          <option value="1920x1080">1080p (1920x1080)</option>
          <option value="1280x720" selected>720p (1280x720)</option>
          <option value="640x480">480p (640x480)</option>
          <option value="640x360">360p (640x360)</option>
        </select>
      </div>
      <div>
        <label>目标视频码率（kbps）</label>
        <input id="videoKbps" type="number" value="1500" />
      </div>
      <div>
        <label>帧率（fps）</label>
        <input id="fps" type="number" value="30" />
      </div>
      <div>
        <label>音频开关</label>
        <select id="audio">
          <option value="on" selected>开</option>
          <option value="off">关</option>
        </select>
      </div>

      <div>
        <label>ICE/STUN（可选）</label>
        <input id="stun" placeholder="例如: stun:stun.l.google.com:19302" />
      </div>
      <div>
        <label>TURN（可选，username:password@host:port?transport=udp）</label>
        <input id="turn" placeholder="例如: turn:turn.example.com:3478?transport=udp" />
      </div>
    </div>

    <div class="row mt">
      <button id="btnStart">开始推流</button>
      <button id="btnStop" class="danger" disabled>停止</button>
      <button id="btnMute" class="secondary" disabled>静音</button>
      <span class="badge" id="state">Idle</span>
    </div>

    <div class="grid mt">
      <div class="video-wrap">
        <video id="local" playsinline autoplay muted></video>
      </div>
      <div>
        <label>状态日志</label>
        <div id="log" class="log"></div>
        <div class="hint mt">
          推流地址（WebRTC）：<code id="webrtcUrl">webrtc://host/app/stream</code><br/>
          API：<code id="apiUrl">http://host:1985/rtc/v1/publish/</code>
        </div>
      </div>
    </div>
  </div>
</div>

<script>
(function(){
  const $ = (id)=>document.getElementById(id);
  const logEl = $("log");
  const stateEl = $("state");
  const localVideo = $("local");
  const btnStart = $("btnStart");
  const btnStop = $("btnStop");
  const btnMute = $("btnMute");

  let pc = null;
  let localStream = null;
  let videoSender = null;
  let audioSender = null;

  function log(...args){
    const line = args.map(a => (typeof a === 'object'? JSON.stringify(a): String(a))).join(' ');
    const time = new Date().toLocaleTimeString();
    logEl.textContent += `[${time}] ${line}\n`;
    logEl.scrollTop = logEl.scrollHeight;
    console.log(...args);
  }

  function setState(s){
    stateEl.textContent = s;
  }

  function buildUrls(){
    const host = $("host").value.trim();
    const apiPort = $("apiPort").value.trim() || "1985";
    const app = $("app").value.trim() || "live";
    const stream = $("stream").value.trim() || "camera";
    const api = `${location.protocol === "https:" ? "https" : "http"}://${host}:1985/rtc/v1/publish/`;
    const webrtc = `webrtc://${host}/${app}/${stream}`;
    $("apiUrl").textContent = api;
    $("webrtcUrl").textContent = webrtc;
    return {api, webrtc, app, stream, host};
  }

  async function start(){
    btnStart.disabled = true;
    setState("准备中…");

    try{
      const {api, webrtc} = buildUrls();

      // 解析分辨率与参数
      const [vw, vh] = ($("resolution").value || "1280x720").split("x").map(Number);
      const fps = Math.max(1, Number($("fps").value || 30));
      const needAudio = $("audio").value === "on";

      // 采集本地媒体
      const constraints = {
        video: {
          width: { ideal: vw }, height: { ideal: vh },
          frameRate: { ideal: fps, max: fps }
        },
        audio: needAudio
      };

      log("getUserMedia constraints:", constraints);
      localStream = await navigator.mediaDevices.getUserMedia(constraints);
      localVideo.srcObject = localStream;

      // 创建 RTCPeerConnection
      const iceServers = [];
      const stun = $("stun").value.trim();
      const turn = $("turn").value.trim();
      if (stun) iceServers.push({ urls: stun });
      if (turn) {
        // 支持 'username:password@host:port'
        const m = turn.match(/^turns?:\/\/?([^@]+)@(.+)$/) || turn.match(/^turns?:(.+)$/);
        if (m) {
          if (turn.includes("@")) {
            const [, cred, url] = m;
            const [username, password] = cred.split(":");
            iceServers.push({ urls: `turn:${url}`, username, credential: password });
          } else {
            iceServers.push({ urls: `turn:${m[1]}` });
          }
        } else {
          // 原样放入
          iceServers.push({ urls: turn });
        }
      }
      const pcConfig = { iceServers, bundlePolicy: "max-bundle" };
      pc = new RTCPeerConnection(pcConfig);
      log("RTCPeerConnection config:", pcConfig);

      // 添加本地轨道
      for (const track of localStream.getVideoTracks()){
        videoSender = pc.addTrack(track, localStream);
      }
      if (needAudio){
        for (const track of localStream.getAudioTracks()){
          audioSender = pc.addTrack(track, localStream);
        }
      }

      // 设置编码参数（码率）
      const kbps = Math.max(100, Number($("videoKbps").value || 1500));
      if (videoSender && videoSender.getParameters) {
        const params = videoSender.getParameters();
        if (!params.encodings || !params.encodings.length) params.encodings = [{}];
        params.encodings[0].maxBitrate = kbps * 1000; // bps
        await videoSender.setParameters(params);
        log(`设置视频 maxBitrate ≈ ${kbps}kbps`);
      }

      pc.oniceconnectionstatechange = () => log("iceConnectionState:", pc.iceConnectionState);
      pc.onconnectionstatechange   = () => {
        log("connectionState:", pc.connectionState);
        setState(pc.connectionState || "Connected");
      };
      pc.onicecandidate = (e)=> {
        if (e.candidate) log("ICE candidate gathered");
      };

      // 创建 offer
      const offer = await pc.createOffer({
        offerToReceiveAudio: false,
        offerToReceiveVideo: false
      });
      await pc.setLocalDescription(offer);
      log("Created offer.");

      // 通过 SRS API 交换 SDP
      const body = {
        api,
        sdp: offer.sdp,
        streamurl: webrtc,
        // 可选参数：客户端标识 / IP
        tid: Math.random().toString(36).slice(2),
        clientip: null
      };
      log("POST to SRS:", api, body);

      const res = await fetch(api, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body)
      });
      if (!res.ok) throw new Error(`SRS API 返回 ${res.status}`);
      const data = await res.json();
      if (!data.sdp) throw new Error("SRS 返回缺少 SDP");
      log("SRS answer received.");

      await pc.setRemoteDescription({ type: "answer", sdp: data.sdp });
      setState("Publishing");
      btnStop.disabled = false;
      btnMute.disabled = false;
      log("推流开始。");
    }catch(err){
      log("❌ 错误：", err.message || err);
      setState("Error");
      btnStart.disabled = false;
    }
  }

  async function stop(){
    btnStop.disabled = true;
    btnMute.disabled = true;
    try{
      if (pc){
        pc.getSenders().forEach(s=> { try{ s.track && s.track.stop(); }catch(_){} });
        pc.close();
      }
      if (localStream){
        localStream.getTracks().forEach(t=>t.stop());
      }
      pc = null; localStream = null; videoSender = null; audioSender = null;
      localVideo.srcObject = null;
      setState("Stopped");
      log("推流已停止。");
    }finally{
      btnStart.disabled = false;
    }
  }

  let muted = false;
  function toggleMute(){
    if (!localStream) return;
    muted = !muted;
    localStream.getAudioTracks().forEach(t=>t.enabled = !muted);
    btnMute.textContent = muted ? "取消静音" : "静音";
    log(muted ? "已静音" : "已取消静音");
  }

  // 事件绑定
  btnStart.addEventListener("click", start);
  btnStop.addEventListener("click", stop);
  btnMute.addEventListener("click", toggleMute);

  // 默认回显 webrtc 与 api
  ["host","apiPort","app","stream"].forEach(id=>$(id).addEventListener("input", buildUrls));
  buildUrls();

  // HTTPS 提示
  if (location.protocol !== "https:" && location.hostname !== "localhost") {
    log("提示：非 HTTPS 环境可能无法访问摄像头/麦克风。建议用 https 或在 localhost 调试。");
  }
})();
</script>
</body>
</html>
